The goals / steps of this project are the following:

  • Compute the camera calibration matrix and distortion coefficients given a set of chessboard images.
  • Apply a distortion correction to raw images.
  • Use color transforms, gradients, etc., to create a thresholded binary image.
  • Apply a perspective transform to rectify binary image ("birds-eye view").
  • Detect lane pixels and fit to find the lane boundary.
  • Determine the curvature of the lane and vehicle position with respect to center.
  • Warp the detected lane boundaries back onto the original image.
  • Output visual display of the lane boundaries and numerical estimation of lane curvature and vehicle position.

1. Compute the camera calibration matrix and distortion coefficients given a set of chessboard images

In [1]:
import numpy as np
import cv2
import glob
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
%matplotlib inline

# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*9,3), np.float32)
objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2)

# Arrays to store object points and image points from all the images.
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane.

# Make a list of calibration images
images = glob.glob('camera_cal/calibration*.jpg')
count = 0;

# Step through the list and search for chessboard corners
for fname in images:
    img = cv2.imread(fname)

    # Grayscale the image
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

    # Find the chessboard corners
    ret, corners = cv2.findChessboardCorners(gray, (9,6),None)

    # If found, add object points, image points
    if ret == True:
        objpoints.append(objp)
        imgpoints.append(corners)

        # Draw and display the corners
        img = cv2.drawChessboardCorners(img, (9,6), corners, ret)
        img = img[...,::-1]  # RGB -> BGR
        plt.figure()
        plt.imshow(img)
        outfile = 'Stage1_checkerdraw_%d.jpg' % (count)
        cv2.imwrite(outfile, img)
        count = count+1
        
        # Camera calibration, given object points, image points, and the shape of the grayscale image
        ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

2. Apply a distortion correction to raw images

In [2]:
def undistort(image): 
    # Undistorting a test image
    dst = cv2.undistort(image, mtx, dist, None, mtx)
    return dst

# Check for one of the calibration image
print('Undistorting a test image')
image_file = 'camera_cal/calibration1.jpg'
img = cv2.imread(image_file)
img = img[...,::-1]  # RGB -> BGR  
undist = undistort(img)
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
f.tight_layout()
ax1.set_title("Original Image", fontsize=30)
ax1.imshow(img)
cv2.imwrite('Stage2_OriginalImage.jpg', img)
ax2.set_title("Undistorted Image", fontsize=30)
ax2.imshow(undist)
cv2.imwrite('Stage2_UndistortedImage.jpg', undist)

# Try undistorting all the test images
images = glob.glob('test_images/test*.jpg')
for image in images:
    img = cv2.imread(image)
    img = img[...,::-1]  # RGB -> BGR  
    undist = undistort(img)
    f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
    f.tight_layout()
    ax1.set_title("Original Image", fontsize=30)
    ax1.imshow(img)
    ax2.set_title("Undistorted Image", fontsize=30)
    ax2.imshow(undist)
    
Undistorting a test image

3. Use color transforms, gradients, etc., to create a thresholded binary image

In [3]:
import matplotlib.image as mpimg

# Edit this function to create your own pipeline.
def convertToBinary(image, r_thresh=(210, 255), s_thresh=(120, 255), sx_thresh=(20, 100)):
    image = np.copy(image)

    R = image[:,:,0]
    r_binary = np.zeros_like(R)
    r_binary[(R > r_thresh[0]) & (R <= r_thresh[1])] = 1 
    
    # Convert to HSV color space and separate the V channel
    hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HLS).astype(np.float)
    l_channel = hsv[:,:,1]
    s_channel = hsv[:,:,2]

    sobelx = cv2.Sobel(l_channel, cv2.CV_64F, 1, 0) # Take the derivative in x
    abs_sobelx = np.absolute(sobelx) # Absolute x derivative to accentuate lines away from horizontal
    scaled_sobel = np.uint8(255*abs_sobelx/np.max(abs_sobelx))
    
    sxbinary = np.zeros_like(scaled_sobel)
    sxbinary[(scaled_sobel >= sx_thresh[0]) & (scaled_sobel <= sx_thresh[1])] = 1
    
    s_binary = np.zeros_like(s_channel)
    s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 1
    # Stack each channel
    color_binary = np.dstack(( np.zeros_like(sxbinary), sxbinary, r_binary))
    
    combined_binary = np.zeros_like(sxbinary)
    combined_binary[(r_binary == 1) | (sxbinary == 1)] = 1   
    return combined_binary
    
binary = convertToBinary(undist)

# Plot the result
print('Thresholded binary image')
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
f.tight_layout()

ax1.imshow(undist)
ax1.set_title('Undistorted Image', fontsize=30)
ax2.imshow(binary, cmap='gray')
cv2.imwrite('Stage3_BinaryImage.jpg', binary * 255)

ax2.set_title('Binary Image', fontsize=30)
plt.subplots_adjust(left=0., right=1, top=0.9, bottom=0.)
Thresholded binary image

4. Apply a perspective transform to rectify binary image ("birds-eye view")

In [4]:
def warper(image):
    # Grab the image shape
    img_size = (image.shape[1], image.shape[0])
    
    offset = 350 # offset for dst points
        
    # Source points
    src = np.float32([[[ 610,  450]], 
                      [[ 680,  450]], 
                      [[ img_size[0]-300,  680]],
                      [[ 380,  680]]])

    # Result points        
    dst = np.float32([[offset, 0], 
                    [img_size[0]-offset, 0], 
                    [img_size[0]-offset, img_size[1]], 
                    [offset, img_size[1]]])
    
    # calculate the perspective transform matrix
    M = cv2.getPerspectiveTransform(src, dst)
    
    # Warp the image 
    warped = cv2.warpPerspective(image, M, img_size)
    
    return warped, M
    
warped, perspective_M = warper(binary)

# Plot the result
print('Top down image')

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
f.tight_layout()
ax1.set_title("Binary Image", fontsize=30)
ax1.imshow(binary, cmap='gray')
ax2.set_title("Top down Image", fontsize=30)
ax2.imshow(warped, cmap='gray')
cv2.imwrite('Stage4_Warped.jpg', warped*255)
Top down image
Out[4]:
True

5. Detect lane pixels and fit to find the lane boundary

In [5]:
# window settings
window_width = 50 
window_height = 80 # Break image into 9 vertical layers since image height is 720
margin = 100 

def window_mask(width, height, img_ref, center,level):
    output = np.zeros_like(img_ref)
    output[int(img_ref.shape[0]-(level+1)*height):int(img_ref.shape[0]-level*height),max(0,int(center-width/2)):min(int(center+width/2),img_ref.shape[1])] = 1
    return output

def find_window_centroids(image, window_width, window_height, margin):
    
    window_centroids = [] # Store the (left,right) window centroid positions per level
    leftx = []
    rightx = []
    
    window = np.ones(window_width) # Create our window template that we will use for convolutions   
    
    # First find the two starting positions for the left and right lane by using np.sum to get the vertical image slice
    # and then np.convolve the vertical image slice with the window template 
    
    l_sum = np.sum(image[int(3*image.shape[0]/4):,:int(image.shape[1]/2)], axis=0)
    l_center = np.argmax(np.convolve(window,l_sum))-window_width/2
    r_sum = np.sum(image[int(3*image.shape[0]/4):,int(image.shape[1]/2):], axis=0)
    r_center = np.argmax(np.convolve(window,r_sum))-window_width/2+int(image.shape[1]/2)
    
    window_centroids.append((l_center,r_center))  
    leftx.append(l_center)
    rightx.append(r_center)
    
    # Go through each layer looking for max pixel locations
    for level in range(1,(int)(image.shape[0]/window_height)):
        # convolve the window into the vertical slice of the image
        image_layer = np.sum(image[int(image.shape[0]-(level+1)*window_height):int(image.shape[0]-level*window_height),:], axis=0)
        conv_signal = np.convolve(window, image_layer)
        offset = window_width/2
        l_min_index = int(max(l_center+offset-margin,0))
        l_max_index = int(min(l_center+offset+margin,image.shape[1]))
        prev_l_center = l_center        
        l_center = np.argmax(conv_signal[l_min_index:l_max_index])+l_min_index-offset      
        r_min_index = int(max(r_center+offset-margin,0))
        r_max_index = int(min(r_center+offset+margin,image.shape[1]))
        prev_r_center = r_center
        r_center = np.argmax(conv_signal[r_min_index:r_max_index])+r_min_index-offset

        window_centroids.append((l_center,r_center))
        leftx.append(l_center)
        rightx.append(r_center)

    return window_centroids, leftx, rightx

window_centroids, leftx, rightx = find_window_centroids(warped, window_width, window_height, margin)

# If window center is found
if len(window_centroids) > 0:

    # Points used to draw all the left and right windows
    l_points = np.zeros_like(warped)
    r_points = np.zeros_like(warped)

    for level in range(0,len(window_centroids)):
        l_mask = window_mask(window_width,window_height,warped,window_centroids[level][0],level)
        r_mask = window_mask(window_width,window_height,warped,window_centroids[level][1],level)
        # Add graphic points from window mask here to total pixels found 
        l_points[(l_points == 255) | ((l_mask == 1) ) ] = 255
        r_points[(r_points == 255) | ((r_mask == 1) ) ] = 255

   
    template = np.array(r_points+l_points,np.uint8) # add both left and right window pixels together
    zero_channel = np.zeros_like(template) # create a zero color channel
    template = np.array(cv2.merge((zero_channel,template,zero_channel)),np.uint8) # make window pixels green
    out_img = np.dstack((warped, warped, warped))*255
    warpage = np.array(out_img,np.uint8) # making the original road pixels 3 color channels
    output = cv2.addWeighted(warpage, 1, template, 0.5, 0.0) # overlay the orignal road image with window results
    
# If no window centers found, just display orginal road image
else:
    output = np.array(cv2.merge((warped,warped,warped)),np.uint8)

# Display the final results
print('Left and right points')
print ('Left: ',leftx)
print ('Right: ',rightx)
print('')
print('Fitted image')

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
f.tight_layout()
ax1.set_title("Warped Image", fontsize=30)
ax1.imshow(warped, cmap='gray')
ax2.set_title("Fitted image", fontsize=30)
ax2.imshow(output)
cv2.imwrite('Stage5_Fitted.jpg', output*255)
Left and right points
Left:  [299.0, 306.0, 328.0, 353.0, 381.0, 425.0, 454.0, 482.0, 532.0]
Right:  [1080.0, 1079.0, 1086.0, 1123.0, 1134.0, 1169.0, 1194.0, 1221.0, 1254.0]

Fitted image
Out[5]:
True

6. Determine the curvature of the lane and vehicle position with respect to center

In [6]:
ploty = np.linspace(0, 719, num=9)

def curvature(leftx, rightx):
    leftx = np.asarray(leftx[::-1])  
    rightx = np.asarray(rightx[::-1])  
    
    # Fit a second order polynomial to pixel positions in each fake lane line
    left_fit = np.polyfit(ploty, leftx, 2)
    left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
    leftx_int = left_fit[0]*720**2 + left_fit[1]*720 + left_fit[2]
    right_fit = np.polyfit(ploty, rightx, 2)
    right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
    rightx_int = right_fit[0]*720**2 + right_fit[1]*720 + right_fit[2]
    position = ((rightx_int+leftx_int)/2)-50
    center = abs(640 - ((rightx_int+leftx_int)/2))
    
    y_eval = np.max(ploty)

    # Define conversions in x and y from pixels space to meters
    ym_per_pix = 30/720 # meters per pixel in y dimension
    xm_per_pix = 3.7/700 # meters per pixel in x dimension

    # Fit new polynomials to x,y in world space
    left_fit_cr = np.polyfit(ploty*ym_per_pix, leftx*xm_per_pix, 2)
    right_fit_cr = np.polyfit(ploty*ym_per_pix, rightx*xm_per_pix, 2)
    
    # Calculate the new radii of curvature
    left_curverad = ((1 + (2*left_fit_cr[0]*y_eval*ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0])
    right_curverad = ((1 + (2*right_fit_cr[0]*y_eval*ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0])

    return position, center, leftx, rightx, left_fitx, right_fitx, left_curverad, right_curverad

position, center, leftx, rightx, left_fitx, right_fitx, left_curverad, right_curverad = curvature(leftx, rightx)

# Plot up the data
mark_size = 3
plt.plot(leftx, ploty, 'o', color='red', markersize=mark_size)
plt.plot(rightx, ploty, 'o', color='blue', markersize=mark_size)
plt.xlim(0, 1280)
plt.ylim(0, 720)
plt.plot(left_fitx, ploty, color='blue', linewidth=3)
plt.plot(right_fitx, ploty, color='blue', linewidth=3)
plt.gca().invert_yaxis() # to visualize as we do the images
plt.savefig('Stage6_DetCurv.jpg')

# Now our radius of curvature is in meters
print('Left:',left_curverad, 'm - Right:', right_curverad, 'm')
# Example values: 632.1 m    626.2 m
Left: 729.2328966835574 m - Right: 781.9238386759345 m

7. Warp the detected lane boundaries back onto the original image

In [7]:
def warpBack(image, warp, persp_M, le_fitx, ri_fitx):
    Minv = np.linalg.inv(persp_M)

    warp_zero = np.zeros_like(warp).astype(np.uint8)
    color_warp = np.dstack((warp_zero, warp_zero, warp_zero))

    pts_left = np.array([np.transpose(np.vstack([le_fitx, ploty]))])
    pts_right = np.array([np.flipud(np.transpose(np.vstack([ri_fitx, ploty])))])
    pts = np.hstack((pts_left, pts_right))

    cv2.fillPoly(color_warp, np.int_([pts]), (0,255,255))

    newwarp = cv2.warpPerspective(color_warp, Minv, (image.shape[1], image.shape[0])) 
    result = cv2.addWeighted(image, 1, newwarp, 0.3, 0)
    return result

warpBk = warpBack(undist, warped, perspective_M, left_fitx, right_fitx)
plt.imshow(warpBk)
warpBk = warpBk[...,::-1]  # BGR -> RGB
cv2.imwrite('Stage7_Warpback.jpg', warpBk)
Out[7]:
True

8. Output visual display of the lane boundaries and numerical estimation of lane curvature and vehicle position

In [8]:
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
f.tight_layout()
ax1.set_title("Original Image", fontsize=30)
ax1.imshow(img)

warpBck = warpBack(undist, warped, perspective_M, left_fitx, right_fitx)
ax2.set_title("Curvature and vehicle position", fontsize=30)

if position == 640:
    cv2.putText(warpBck, 'Vehicle is in the center', (50,100),
                 fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)
elif position < 640:
    cv2.putText(warpBck, 'Vehicle is {:.2f}m left of center'.format(center*3.7/700), (50,100),
         fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)

else:
    cv2.putText(warpBck, 'Vehicle is {:.2f}m right of center'.format(center*3.7/700), (50,100),
                 fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)    

cv2.putText(warpBck, 'Radius of Curvature {}(m)'.format(int((left_curverad + right_curverad)/2)), (50,140),
             fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)

ax2.imshow(warpBck)
warpBck = warpBck[...,::-1]  # BGR -> RGB
cv2.imwrite('Stage8_DispCurvAndVehPosition.jpg', warpBck)
Out[8]:
True
In [9]:
class laneline():
    def __init__(self):
        # was the line detected in the last iteration?
        self.detected = False  
        # x values of the last n fits of the line
        self.recent_xfitted = [] 
        #average x values of the fitted line over the last n iterations
        self.bestx = None     
        #polynomial coefficients averaged over the last n iterations
        self.best_fit = None  
        #polynomial coefficients for the most recent fit
        self.current_fit = [np.array([False])]  
        #radius of curvature of the line in some units
        self.radius_of_curvature = None 
        #distance in meters of vehicle center from the line
        self.line_base_pos = None 
        #difference in fit coefficients between last and new fits
        self.diffs = np.array([0,0,0], dtype='float') 
        #x values for detected line pixels
        self.allx = None  
        #y values for detected line pixels
        self.ally = None

9. The pipeline

In [10]:
def pipeline(im):  
    undistorded = undistort(im)
    final_binary = convertToBinary(undistorded)
    final_warped, final_perspective_M = warper(final_binary)
    window_cent, lex, rix = find_window_centroids(final_warped, window_width, window_height, margin)      
    position, center, leftx, rightx, left_lane.recent_xfitted, right_lane.recent_xfitted, left_lane.radius_of_curvature, right_lane.radius_of_curvature = curvature(lex, rix)

    alpha = 0.5
    if left_lane.bestx is None:
        left_lane.bestx = left_lane.recent_xfitted
    else :    
        left_lane.recent_xfitted = left_lane.recent_xfitted * ( 1 - alpha) + alpha * left_lane.bestx
        
    if right_lane.bestx is None:
        right_lane.bestx = right_lane.recent_xfitted
    else :    
        right_lane.recent_xfitted = right_lane.recent_xfitted * ( 1 - alpha) + alpha * right_lane.bestx  
            
    #Sanity check  
    left_lane.detected = False
    right_lane.detected = False 
    
    max_distance = 0  
    min_distance = 10000
    for i in range(len(left_lane.recent_xfitted)) : 
        point_distance = right_lane.recent_xfitted[i] - left_lane.recent_xfitted[i]
        if point_distance > max_distance:
            max_distance = point_distance
        if point_distance < min_distance:
            min_distance = point_distance    
        
    if (min_distance > 710) and (max_distance < 900):
        left_lane.detected = True
        right_lane.detected = True              
        
    if not left_lane.detected:
        left_lane.recent_xfitted = left_lane.bestx
    else:
        left_lane.bestx = left_lane.recent_xfitted
    if not right_lane.detected:
        right_lane.recent_xfitted = right_lane.bestx
    else:
        right_lane.bestx = right_lane.recent_xfitted
    
    result = warpBack(im, final_warped, final_perspective_M, left_lane.recent_xfitted, right_lane.recent_xfitted)    
    
    # Print distance from center on video
    if (center*3.7/700) < 0.01:
        cv2.putText(result, 'Vehicle is in the center', (50,80),
                 fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)
    elif center < 640:
        cv2.putText(result, 'Vehicle is {:.2f}m left of center'.format(center*3.7/700), (50,80),
                 fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)
    else:
        cv2.putText(result, 'Vehicle is {:.2f}m right of center'.format(center*3.7/700), (50,80),
                 fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)
    # Print radius of curvature on video
    cv2.putText(result, 'Radius of Curvature {}(m)'.format(int((left_lane.radius_of_curvature + right_lane.radius_of_curvature)/2)), (50,140),
             fontFace = 16, fontScale = 1, color=(255,255,255), thickness = 2)
    
    return result

# Test Final Pipeline
print('Test Final Pipeline')
left_lane = laneline()
right_lane = laneline()
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(16,8))
f.tight_layout()
ax1.set_title("Original Image", fontsize=30)
ax1.imshow(img)
ax2.set_title("After Pipeline", fontsize=30)
pipeline_imgoutput = pipeline(img)
ax2.imshow(pipeline_imgoutput)

pipeline_imgoutput = pipeline_imgoutput[...,::-1]  # BGR -> RGB
cv2.imwrite('Stage9_CompleteImagePipeline.jpg', pipeline_imgoutput)
Test Final Pipeline
Out[10]:
True

10. Apply pipeline for the video

In [11]:
# Import everything needed to edit/save/watch video clips
from moviepy.editor import VideoFileClip
from IPython.display import HTML

left_lane = laneline()
right_lane = laneline()
white_output = 'project_video_result.mp4'
clip1 = VideoFileClip("project_video.mp4")#.subclip(38,43)
white_clip = clip1.fl_image(pipeline)
%time white_clip.write_videofile(white_output, audio=False)

HTML("""
<video width="960" height="540" controls>
  <source src="{0}">
</video>
""".format(white_output))
[MoviePy] >>>> Building video project_video_result.mp4
[MoviePy] Writing video project_video_result.mp4
100%|█████████████████████████████████████████████████████████████████████████████▉| 1260/1261 [03:53<00:00,  6.90it/s]
[MoviePy] Done.
[MoviePy] >>>> Video ready: project_video_result.mp4 

Wall time: 3min 55s
Out[11]: